package jmine.tec.web.servlet.cipher;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import jmine.tec.utils.Tuple;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Filtro para implementar comunicacao cifrada. Implementa um protocolo de 'handshake' para troca de chaves entre o servidor e o cliente.
 * Servlets e filtros que venham depois podem usar os metodos do request normalmente. Idealmente, esta classe deve ser sobrescrita nas
 * aplicacoes ou configurada no spring para receber o {@link #initialSecureCommunication}
 * 
 * @author takeshi
 */
public class SecureCommunicationFilter implements Filter {

    private final Log logger = LogFactory.getLog(SecureCommunicationFilter.class);

    public static final String COMM_IN_SESSION = SecureCommunicationFilter.class.getName() + "#comm";

    private SecureCommunicationHandler initialSecureCommunication;

    private static final ThreadLocal<Boolean> SECURE_CHANNEL = new ThreadLocal<Boolean>();

    /**
     * {@inheritDoc}
     */
    public void destroy() {
    }

    /**
     * {@inheritDoc}
     */
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        SECURE_CHANNEL.remove();
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        final HttpSession httpSession = httpRequest.getSession();
        synchronized (httpSession) {
            SecureCommunicationHandler comm = getCommunication(httpRequest);
            if (comm == null) {
                httpResponse.sendError(SecureCommunicationProtocol.INITIALIZATION_NEEDED);
            } else {
                if (comm.proceedRequest(httpRequest, httpResponse)) {
                    SECURE_CHANNEL.set(Boolean.TRUE);
                    try {
                        Tuple<HttpServletRequest, HttpServletResponse> wrapped = comm.wrapRequestResponse(httpRequest, httpResponse);
                        chain.doFilter(wrapped.head(), wrapped.tail());
                        wrapped.tail().flushBuffer();
                    } finally {
                        SECURE_CHANNEL.remove();
                    }
                } else {
                    httpResponse.setCharacterEncoding("UTF-8"); // must ALWAYS use UTF-8 when negotiating
                    SecureCommunicationHandler nextStage = comm.nextStage(httpRequest, httpResponse);
                    if (nextStage == null) {
                        // something went wrong in the conversation.
                        httpSession.invalidate();
                        httpResponse.sendError(HttpServletResponse.SC_EXPECTATION_FAILED);
                    } else {
                        httpSession.setAttribute(COMM_IN_SESSION, nextStage);
                    }
                }
            }
        }
    }

    /**
     * @return boolean
     */
    public static boolean isSecureChannel() {
        return Boolean.TRUE.equals(SECURE_CHANNEL.get());
    }

    /**
     * Devolve o {@link SecureCommunicationHandler} na sessao ou inicializa um novo se for um GET com um parametro initialize=true
     * 
     * @param httpRequest {@link HttpServletRequest}
     * @return {@link SecureCommunicationHandler}
     */
    protected SecureCommunicationHandler getCommunication(HttpServletRequest httpRequest) {
        HttpSession session = httpRequest.getSession(true);
        synchronized (session) {
            if ("GET".equalsIgnoreCase(httpRequest.getMethod())
                    && "true".equals(httpRequest.getParameter(SecureCommunicationProtocol.INITIALIZATION_PARAMETER_NAME))) {
                // // client specifically asked for a handshake
                httpRequest.getSession().removeAttribute(COMM_IN_SESSION);
                return initialize();
            }
            SecureCommunicationHandler state = (SecureCommunicationHandler) httpRequest.getSession().getAttribute(COMM_IN_SESSION);
            if (state == null) {
                return null;
            }
            return state;
        }
    }

    /**
     * @return {@link SecureCommunicationHandler}
     */
    private SecureCommunicationHandler initialize() {
        logger.debug("Initializing SecureCommunicatonState");
        return this.getInitialSecureCommunication();
    }

    /**
     * {@inheritDoc}
     */
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    /**
     * @return the initialSecureCommunication
     */
    public SecureCommunicationHandler getInitialSecureCommunication() {
        return initialSecureCommunication;
    }

    /**
     * @param initialSecureCommunication the initialSecureCommunication to set
     */
    public void setInitialSecureCommunication(SecureCommunicationHandler initialSecureCommunication) {
        this.initialSecureCommunication = initialSecureCommunication;
    }

}
